﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Altairis.OtpAuth.Core;
using System.Security.Cryptography;
using System.Xml.Serialization;
using System.IO;

namespace OtpAuthTest {
    class Program {
        private static readonly byte[] SECRET_20B = Encoding.ASCII.GetBytes("12345678901234567890");
        private static readonly byte[] SECRET_32B = Encoding.ASCII.GetBytes("12345678901234567890123456789012");
        private static readonly byte[] SECRET_64B = Encoding.ASCII.GetBytes("1234567890123456789012345678901234567890123456789012345678901234");
        private static readonly DateTime[] TOTP_TEST_TIMES = { 
            new DateTime(1970, 01, 01, 00, 00, 59, DateTimeKind.Utc),
            new DateTime(2005, 03, 18, 01, 58, 29, DateTimeKind.Utc),
            new DateTime(2009, 02, 13, 23, 31, 30, DateTimeKind.Utc),
            new DateTime(2033, 05, 18, 03, 33, 20, DateTimeKind.Utc),
            new DateTime(2603, 10, 11, 11, 33, 20, DateTimeKind.Utc),
            DateTime.UtcNow,
        };

        static void Main(string[] args) {
            TestHashOtp();
            TestTimeOtp();
            TestGoogleAuthenticator();
            TestTokenSerialization();
        }

        private static void TestHashOtp() {
            Console.WriteLine("Testing event-based OTP (HOTP) generator:");

            var hotpGenerator = new EventOtpGenerator {
                Digits = 6,
                UseChecksumDigit = false
            };
            var otps = new string[10];
            for (int i = 0; i < 10; i++) {
                otps[i] = hotpGenerator.GenerateOneTimePassword(SECRET_20B, i);
                Console.WriteLine("{0,-3}{1}", i, otps[i]);
            }
            Console.WriteLine("The above values should match test values published in RFC 4226.");
            Console.WriteLine();

            Console.WriteLine("Testing HOTP verifier, ThrottlingCount=3");
            var hotpVerifier = new EventOtpVerifier {
                Secret = SECRET_20B,
                ThrottlingCount = 3,
                Counter = 0,
            };
            var tasks = new[] { 
                new { Name = "Valid password", Index = 0, Expected = true }, 
                new { Name = "Replay attack", Index = 0, Expected = false },
                new { Name = "Out-of-sync within threshold", Index = 3, Expected = true},
                new { Name = "Out-of-sync", Index = 9, Expected = false},
                new { Name = "Hole attack", Index = 2, Expected = false},
                new { Name = "Valid password after resync", Index = 4, Expected = true},
            };
            foreach (var task in tasks) {
                var result = hotpVerifier.VerifyOneTimePassword(otps[task.Index]);
                Console.WriteLine("  {0,-30} ctr {3}, idx {4}, res/exp {1}/{2}",
                    task.Name,              // 0
                    result,                 // 1
                    task.Expected,          // 2
                    hotpVerifier.Counter,   // 3
                    task.Index);            // 4
            }
            Console.WriteLine();
        }

        private static void TestTimeOtp() {
            Console.WriteLine("Time-based OTP (TOTP):");
            var otpSha1 = new TimeOtpGenerator {
                HmacAlgorithmName = "HMACSHA1",
                Digits = 8,
                TimeStep = 30,
                UseChecksumDigit = false
            };
            var otpSha256 = new TimeOtpGenerator {
                HmacAlgorithmName = "HMACSHA256",
                Digits = 8,
                TimeStep = 30,
                UseChecksumDigit = false
            };
            var otpSha512 = new TimeOtpGenerator {
                HmacAlgorithmName = "HMACSHA512",
                Digits = 8,
                TimeStep = 30,
                UseChecksumDigit = false
            };
            Console.WriteLine("{0,-20} {1,-10}{2,-10}{3,-10}", "UTC time", "SHA-1", "SHA-256", "SHA-512");
            foreach (var instant in TOTP_TEST_TIMES) {
                Console.WriteLine("{0:yyyy-MM-dd HH:mm:ss}  {1,-10}{2,-10}{3,-10}",
                    instant.ToUniversalTime(),                              // 0
                    otpSha1.GenerateOneTimePassword(SECRET_20B, instant),    // 1
                    otpSha256.GenerateOneTimePassword(SECRET_32B, instant),  // 2
                    otpSha512.GenerateOneTimePassword(SECRET_64B, instant)); // 3
            }
            Console.WriteLine("The above values (except the last row, based on current time) should match");
            Console.WriteLine("test values published in RFC 6238.");
            Console.WriteLine();

            Console.WriteLine("Testing TOTP verifier, TimeStep=30, ThrottlingCount=1:");
            var otpv = new TimeOtpVerifier {
                Secret = SECRET_20B,
                ThrottlingCount = 1
            };

            var tasks = new[] { 
                new { Name = "Current time", Expected = true, Instant = DateTime.UtcNow  }, 
                new { Name = "Early (within threshold)", Expected = true, Instant = DateTime.UtcNow.AddSeconds(30)},
                new { Name = "Late (within threshold)", Expected = true, Instant = DateTime.UtcNow.AddSeconds(-30)},
                new { Name = "Too early", Expected = false, Instant = DateTime.UtcNow.AddSeconds(60)},
                new { Name = "Too late", Expected = false, Instant = DateTime.UtcNow.AddSeconds(-60)},
            };
            foreach (var task in tasks) {
                var otp = otpSha1.GenerateOneTimePassword(SECRET_20B, task.Instant);
                var result = otpv.VerifyOneTimePassword(otp);
                Console.WriteLine("  {0,-30} ({3:yyyy-MM-dd HH:mm:ss}) res/exp {1}/{2}",
                    task.Name,              // 0
                    result,                 // 1
                    task.Expected,          // 2
                    task.Instant);          // 3
            }
            Console.WriteLine();
        }

        private static void TestGoogleAuthenticator() {
            Console.WriteLine("Testing Google Authenticator compatible generation");

            var googleAuthenticatorToken = new TimeOtpToken {
                Name = "Google Authenticator",
                Secret = Base32.FromBase32String("MYSECRET"),
                Generator = new TimeOtpGenerator {
                    Digits = 6
                }
            };

            Console.WriteLine("Secret (Base32-encoded): MYSECRET");
            Console.WriteLine("Current OTP:             {0}", googleAuthenticatorToken.GenerateCurrentOneTimePassword());
        }

        private static void TestTokenSerialization() {
            var t1 = new EventOtpToken {
                Name = "Test HOTP token",
                Description = "This is token intended to do serialization testing",
                Secret = SECRET_20B,
                Generator = new EventOtpGenerator {
                    Digits = 6
                }
            };
            Console.Write("Serializing HOTP token to _hotp.xml...");
            SaveTokenToFile(t1, "_hotp.xml");
            Console.WriteLine("OK");

            var t2 = new TimeOtpToken {
                Name = "Test TOTP token",
                Description = "This is token intended to do serialization testing",
                Secret = SECRET_20B,
                Generator = new TimeOtpGenerator {
                    Digits = 6
                }
            };
            Console.Write("Serializing TOTP token to _totp.xml...");
            SaveTokenToFile(t2, "_totp.xml");
            Console.WriteLine("OK");

            var c = new TokenCollection { t1, t2 };
            Console.Write("Serializing collection to _collection.xml...");
            SaveTokenToFile(c, "_collection.xml");
            Console.WriteLine("OK");
        }

        private static void SaveTokenToFile(object token, string fileName) {
            using (var fs = File.Create(fileName)) {
                var xs = new XmlSerializer(token.GetType());
                xs.Serialize(fs, token);
            }
        }

    }
}
